//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using EffiProz.Core.Lib;
using EffiProz.Core.ScriptIO;
using EffiProz.Core.Errors;

namespace EffiProz.Core.Persist
{
    // fredt@users 20020215 - patch 1.7.0 by fredt
    // to move operations on the database.properties files to new
    // class HsqlDatabaseProperties
    // fredt@users 20020220 - patch 488200 by xclayl@users - throw exception
    // throw addded to all methods relying on file io
    // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP)
    // fredt@users 20020405 - patch 1.7.0 by fredt - no change _in db location
    // because important information about the database is now stored _in the
    // *.properties file, all database files should be _in the same folder as the
    // *.properties file
    // tony_lai@users 20020820 - export hsqldb.log_size to .properties file
    // tony_lai@users 20020820 - changes to shutdown compact to save memory
    // fredt@users 20020910 - patch 1.7.1 by Nitin Chauhan - code improvements
    // fredt@users 20021208 - ongoing revamp
    // fredt@users 20021212 - do not rewrite the *.backup file if the *.data
    // file has not been updated _in the current session.
    // boucherb@users 20030510 - patch 1.7.2 consolidated all periodic database
    // tasks _in one timed task queue
    // fredt@users - 20050102 patch 1.8.0 - refactoring and clearer separation of concerns

    /**
     *  This class is responsible for managing the database files. An HSQLDB database
     *  consists of a .properties file, a .script file (contains an SQL script),
     *  a .data file (contains data of cached tables) a .backup file
     *  and a .log file.<p>
     *  When using TEXT tables, a data source for each table is also present.<p>
     *
     *  Notes on OpenOffice.org integration.
     *
     *  A Storage API is used when HSQLDB is integrated into OpenOffice.org. All
     *  file operations on the 4 main files are performed by OOo, which integrates
     *  the contents of these files into its database file. The script format is
     *  always TEXT _in this case.
     *
     * Extensively rewritten and extended _in successive versions of HSQLDB.
     *
     * @author Thomas Mueller (Hypersonic SQL Group)
     * @author fredt@users.
     * @version 1.8.0
     * @since Hypersonic SQL
     */
    public class Log
    {

        private EffiProzDatabaseProperties properties;
        private String fileName;
        private Database database;
        private EfzFileAccess fa;
        public ScriptWriterBase dbLogWriter;
        private String scriptFileName;
        private String logFileName;
        private bool filesReadOnly;
        private long maxLogSize;
        private int writeDelay;
        private int scriptFormat;
        private DataFileCache cache;

        // We are using persist.Logger-instance-specific FrameworkLogger
        // because it is Database-instance specific.
        // If add any static level logging, should instantiate a standard,
        // context-agnostic FrameworkLogger for that purpose.
        public Log(Database db)
        {

            database = db;
            fa = db.logger.getFileAccess();
            fileName = db.getPath();
            properties = db.getProperties();
        }

        public void initParams()
        {

            // Allows the user to set log size in the properties file.
            maxLogSize = database.logger.propLogSize * 1024L * 1024;
            scriptFormat = 0;
            writeDelay = database.logger.propWriteDelay;
            filesReadOnly = database.isFilesReadOnly();
            scriptFileName = fileName + ".script";
            logFileName = fileName + ".log";
        }

        /**
         * When opening a database, the hsqldb.compatible_version property is
         * used to determine if this version of the engine is equal to or greater
         * than the earliest version of the engine capable of opening that
         * database.<p>
         *
         * @throws  CoreException
         */
        public void open()
        {

            initParams();

            int state = properties.getDBModified();

            switch (state)
            {

                case EffiProzDatabaseProperties.FILES_MODIFIED:
                    deleteNewAndOldFiles();
                    processScript();
                    processLog();
                    close(false);

                    if (cache != null)
                    {
                        cache.open(filesReadOnly);
                    }

                    reopenAllTextCaches();
                    break;

                case EffiProzDatabaseProperties.FILES_NEW:
                    renameNewDataFile();
                    renameNewBackup();
                    renameNewScript();
                    deleteLog();
                    properties.setDBModified(
                        EffiProzDatabaseProperties.FILES_NOT_MODIFIED);

                    // continue as non-modified files
                    // fall through
                    goto case EffiProzDatabaseProperties.FILES_NOT_MODIFIED;
                case EffiProzDatabaseProperties.FILES_NOT_MODIFIED:

                    /**
                     * if startup is after a SHUTDOWN SCRIPT and there are CACHED
                     * or TEXT tables, perform a checkpoint so that the .script
                     * file no longer contains CACHED or TEXT table rows.
                     */
                    processScript();

                    if (isAnyCacheModified())
                    {
                        properties.setDBModified(
                            EffiProzDatabaseProperties.FILES_MODIFIED);
                        close(false);

                        if (cache != null)
                        {
                            cache.open(filesReadOnly);
                        }

                        reopenAllTextCaches();
                    }
                    break;
            }

            openLog();

            if (!filesReadOnly)
            {
                properties.setDBModified(EffiProzDatabaseProperties.FILES_MODIFIED);
            }
        }

        /**
         * Close all the database files. If script argument is true, no .data
         * or .backup file will remain and the .script file will contain all the
         * data of the cached tables as well as memory tables.
         *
         * This is not used for filesReadOnly databases which use shutdown.
         */
        public void close(bool script)
        {

            closeLog();
            deleteNewAndOldFiles();
            writeScript(script);
            closeAllTextCaches(script);

            if (cache != null)
            {
                cache.close(true);
            }

            // set this one last to save the props
            properties.setDBModified(EffiProzDatabaseProperties.FILES_NEW);
            deleteLog();

            if (script)
            {
                deleteBackup();
                deleteData();
            }
            else
            {
                try
                {
                    backupData();
                    renameNewBackup();
                }
                catch (IOException ) { }
            }

            renameNewScript();
            properties.setDBModified(EffiProzDatabaseProperties.FILES_NOT_MODIFIED);
        }

        /**
         * Fast counterpart to close(). Does not perform a checkpoint or a backup
         * of the .data file.
         */
        public void shutdown()
        {

            synchLog();

            if (cache != null)
            {
                cache.close(false);
            }

            closeAllTextCaches(false);
            closeLog();
        }

        /**
         * Deletes the leftovers from any previous unfinished operations.
         */
        public void deleteNewAndOldFiles()
        {

            fa.removeElement(fileName + ".data" + ".old");
            fa.removeElement(fileName + ".data" + ".new");
            fa.removeElement(fileName + ".backup" + ".new");
            fa.removeElement(scriptFileName + ".new");
        }

        public void deleteBackup()
        {
            fa.removeElement(fileName + ".backup");
        }

        public void deleteData()
        {
            fa.removeElement(fileName + ".data");
        }

        public void backupData()
        {
#if !SILVERLIGHT
            if (database.logger.propIncrementBackup)
            {
                fa.removeElement(fileName + ".backup");

                return;
            }

            if (fa.IsStreamElement(fileName + ".data"))
            {

                FileArchiver.archive(fileName + ".data", fileName + ".backup.new",
                                     database.logger.getFileAccess(),
                                     FileArchiver.COMPRESSION_ZIP);
            }

#endif

        }

        public void renameNewDataFile()
        {

            if (fa.IsStreamElement(fileName + ".data.new"))
            {
                fa.RenameElement(fileName + ".data.new", fileName + ".data");
            }
        }

        public void renameNewBackup()
        {

            if (fa.IsStreamElement(fileName + ".backup.new"))
            {
                fa.RenameElement(fileName + ".backup.new", fileName + ".backup");
            }
        }

        public void renameNewScript()
        {

            if (fa.IsStreamElement(scriptFileName + ".new"))
            {
                fa.RenameElement(scriptFileName + ".new", scriptFileName);
            }
        }

        public void deleteNewScript()
        {
            fa.removeElement(scriptFileName + ".new");
        }

        public void deleteNewBackup()
        {
            fa.removeElement(fileName + ".backup.new");
        }

        public void deleteLog()
        {
            fa.removeElement(logFileName);
        }

        /**
         * Checks all the caches and returns true if the modified flag is set for any
         */
        public bool isAnyCacheModified()
        {

            if (cache != null && cache.isFileModified())
            {
                return true;
            }

            return isAnyTextCacheModified();
        }

        /**
         * Performs checkpoint including pre and post operations. Returns to the
         * same state as before the checkpoint.
         */
        public void checkpoint(bool defr)
        {

            if (filesReadOnly)
            {
                return;
            }

            if (cache == null)
            {
                defr = false;
            }
            else if (forceDefrag())
            {
                defr = true;
            }

            if (defr)
            {
                try
                {
                    defrag();

                    return;
                }
                catch (Exception )
                {
                    database.logger.checkpointDisabled = true;

                    return;
                }
            }

            bool result = checkpointClose();

            if (result)
            {
                checkpointReopen();
            }
        }

        /**
         * Performs checkpoint including pre and post operations. Returns to the
         * same state as before the checkpoint.
         */
        public bool checkpointClose()
        {

            if (filesReadOnly)
            {
                return true;
            }

            deleteNewAndOldFiles();

            try
            {
                writeScript(false);
            }
            catch (CoreException)
            {
                deleteNewScript();

                return false;
            }

            try
            {
                if (cache != null)
                {
                    cache.close(true);
                    cache.backupFile();
                }
            }
            catch (Exception )
            {

                // backup failed perhaps due to lack of disk space
                deleteNewScript();
                deleteNewBackup();

                try
                {
                    if (!cache.isFileOpen())
                    {
                        cache.open(false);
                    }
                }
                catch (Exception ) { }

                return false;
            }

            properties.setDBModified(EffiProzDatabaseProperties.FILES_NEW);
            closeLog();
            deleteLog();
            renameNewScript();
            renameNewBackup();

            try
            {
                properties.setDBModified(
                    EffiProzDatabaseProperties.FILES_NOT_MODIFIED);
            }
            catch (Exception ) { }

            return true;
        }

        public bool checkpointReopen()
        {

            if (filesReadOnly)
            {
                return true;
            }

            try
            {
                if (cache != null)
                {
                    cache.open(false);
                }

                if (dbLogWriter != null)
                {
                    openLog();
                }

                properties.setDBModified(EffiProzDatabaseProperties.FILES_MODIFIED);
            }
            catch (Exception )
            {
                return false;
            }

            return true;
        }

        /**
         *  Writes out all the rows to a new file without fragmentation.
         */
        public void defrag()
        {

            if (cache.fileFreePosition == DataFileCache.INITIAL_FREE_POS)
            {
                return;
            }

            database.logger.logInfoEvent("defrag start");

            try
            {

                // test
                /*            {
                                Session session = database.getSessionManager().getSysSession();
                                HsqlArrayList allTables =
                                    database.schemaManager.getAllTables();

                                for (int i = 0, tSize = allTables.size(); i < tSize; i++) {
                                    Table t     = (Table) allTables.get(i);
                                    int   count = 0;

                                    if (t.getTableType() == TableBase.CACHED_TABLE) {
                                        RowIterator it = t.rowIterator(session);

                                        for (; it.hasNext(); count++) {
                                            CachedObject row = it.getNextRow();
                                        }

                                        System.out.println("table " + t.getName().name + " "
                                                           + count);
                                    }
                                }
                            }
                */

                //
                DataFileDefrag dfd = cache.defrag();
            }
            catch (CoreException e)
            {
                database.logger.logSevereEvent("defrag failure", e);

                throw (CoreException)e;
            }
            catch (Exception e)
            {
                database.logger.logSevereEvent("defrag failure", e);

                throw Error.error(ErrorCode.DATA_FILE_ERROR, e);
            }

            // test
            /*
                    {
                        Session session = database.getSessionManager().getSysSession();
                        HsqlArrayList allTables = database.schemaManager.getAllTables();

                        for (int i = 0, tSize = allTables.size(); i < tSize; i++) {
                            Table t     = (Table) allTables.get(i);
                            int   count = 0;

                            if (t.getTableType() == Table.CACHED_TABLE) {
                                RowIterator it = t.rowIterator(session);

                                for (; it.hasNext(); count++) {
                                    CachedObject row = it.getNextRow();
                                }

                                System.out.println("table " + t.getName().name + " "
                                                   + count);
                            }
                        }
                    }
            */

            //
            database.logger.logInfoEvent("defrag end");
        }

        /**
         * Returns true if lost space is above the threshold percentage
         */
        public bool forceDefrag()
        {

            long limit = database.logger.propCacheDefragLimit
                         * cache.getFileFreePos() / 100;
            long lostSize = cache.freeBlocks.getLostBlocksSize();

            return limit > 0 && lostSize > limit;
        }

        /**
         *
         */
        public bool hasCache()
        {
            return cache != null;
        }

        /**
         * Responsible for creating the cache instance.
         */
        public DataFileCache getCache()
        {

            if (cache == null)
            {
                cache = new DataFileCache(database, fileName);

                cache.open(filesReadOnly);
            }

            return cache;
        }

        public void setLogSize(int megas)
        {
            maxLogSize = megas * 1024L * 1024;
        }

        public int getScriptType()
        {
            return scriptFormat;
        }

        /**
         * Write delay specifies the frequency of FileDescriptor.sync() calls.
         */
        public int getWriteDelay()
        {
            return writeDelay;
        }

        public void setWriteDelay(int delay)
        {

            writeDelay = delay;

            if (dbLogWriter != null)
            {
                synchLog();
                dbLogWriter.setWriteDelay(delay);
            }
        }

        public void setIncrementBackup(bool val)
        {

            if (cache != null)
            {
                cache.setIncrementBackup(val);

                cache.fileModified = true;
            }
        }

        /**
         * Various writeXXX() methods are used for logging statements.
         */
        public void writeStatement(Session session, String s)
        {

            try
            {
                dbLogWriter.writeLogStatement(session, s);
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName);
            }

            if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize)
            {
                database.logger.checkpointRequired = true;
            }
        }

        public void writeInsertStatement(Session session, Table t, Object[] row)
        {

            try
            {
                dbLogWriter.writeInsertStatement(session, t, row);
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName);
            }

            if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize)
            {
                database.logger.checkpointRequired = true;
            }
        }

        public void writeDeleteStatement(Session session, Table t, Object[] row)
        {

            try
            {
                dbLogWriter.writeDeleteStatement(session, t, row);
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName);
            }

            if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize)
            {
                database.logger.checkpointRequired = true;
            }
        }

        public void writeSequenceStatement(Session session, NumberSequence s)
        {

            try
            {
                dbLogWriter.writeSequenceStatement(session, s);
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName);
            }

            if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize)
            {
                database.logger.checkpointRequired = true;
            }
        }

        public void writeCommitStatement(Session session)
        {

            try
            {
                dbLogWriter.writeCommitStatement(session);
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName);
            }

            if (maxLogSize > 0 && dbLogWriter.size() > maxLogSize)
            {
                database.logger.checkpointRequired = true;
            }
        }

        public void synchLog()
        {

            if (dbLogWriter != null)
            {
                dbLogWriter.sync();
            }
        }

        /**
         * Wrappers for openning-starting / stoping-closing the log file and
         * writer.
         */
        public void openLog()
        {

            if (filesReadOnly)
            {
                return;
            }
#if !SILVERLIGHT
            Crypto crypto = database.logger.getCrypto();
#endif
            try
            {
#if !SILVERLIGHT
                if (crypto == null)
                {
                    dbLogWriter = new ScriptWriterText(database, logFileName,
                                                       false, false, false);
                }
                else
                {
                    dbLogWriter = new ScriptWriterEncode(database, logFileName,
                                                         crypto);
                }
#else
                dbLogWriter = new ScriptWriterText(database, logFileName,
                                                    false, false, false);
#endif

                dbLogWriter.setWriteDelay(writeDelay);
                dbLogWriter.start();
            }
            catch (Exception )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, logFileName);
            }
        }

        public void closeLog()
        {

            lock (this)
            {
                if (dbLogWriter != null)
                {
                    dbLogWriter.close();
                }
            }
        }

        /**
         * Write the .script file as .script.new.
         */
        public void writeScript(bool full)
        {

            deleteNewScript();

            ScriptWriterBase scw;
#if !SILVERLIGHT
            Crypto crypto = database.logger.getCrypto();

            if (crypto == null)
            {
                scw = new ScriptWriterText(database, scriptFileName + ".new",
                                           full, true, false);
            }
            else
            {
                scw = new ScriptWriterEncode(database, scriptFileName + ".new",
                                             full, crypto);
            }
#else
            scw = new ScriptWriterText(database, scriptFileName + ".new",
                                          full, true, false);
#endif

            scw.writeAll();
            scw.close();
        }

        /**
         * Performs all the commands in the .script file.
         */
        private void processScript()
        {

            ScriptReaderBase scr = null;

            try
            {
                if (fa.IsStreamElement(scriptFileName))
                {
#if !SILVERLIGHT && !PLATFORM_COMPACTFRAMEWORK
                    Crypto crypto = database.logger.getCrypto();

                    if (crypto == null)
                    {
                        scr = new ScriptReaderText(database, scriptFileName);
                    }
                    else
                    {
                        scr = new ScriptReaderDecode(database, scriptFileName,
                                                     crypto);
                    }
#else
                    scr = new ScriptReaderText(database, scriptFileName);
#endif

                    Session session =
                        database.sessionManager.getSysSessionForScript(database);

                    scr.readAll(session);
                    scr.close();
                }
            }
            catch (Exception e)
            {
                if (scr != null)
                {
                    scr.close();

                    if (cache != null)
                    {
                        cache.close(false);
                    }

                    closeAllTextCaches(false);
                }

                database.logger.logWarningEvent("Script processing failure", e);

                if (e is CoreException)
                {
                    throw (CoreException)e;
                }
                else if (e is IOException)
                {
                    throw Error.error(ErrorCode.FILE_IO_ERROR, e);
                }
                else if (e is OutOfMemoryException)
                {
                    throw Error.error(ErrorCode.OUT_OF_MEMORY);
                }
                else
                {
                    throw Error.error(ErrorCode.GENERAL_ERROR, e);
                }
            }
        }

        /**
         * Performs all the commands in the .log file.
         */
        private void processLog()
        {

            if (fa.IsStreamElement(logFileName))
            {
                ScriptRunner.runScript(database, logFileName);
            }
        }

        /**
         * Restores a compressed backup or the .data file.
         */
        private void restoreBackup()
        {
#if !SILVERLIGHT
            if (database.logger.propIncrementBackup)
            {
                restoreBackupIncremental();

                return;
            }

            // in case data file cannot be deleted, reset it
            DataFileCache.deleteOrResetFreePos(database, fileName + ".data");

            try
            {
                FileArchiver.unarchive(fileName + ".backup", fileName + ".data",
                                       database.logger.getFileAccess(),
                                       FileArchiver.COMPRESSION_ZIP);
            }
            catch (Exception t)
            {
                throw Error.error(t, ErrorCode.FILE_IO_ERROR,
                                  ErrorCode.M_Message_Pair, new Object[] {
                t.Message, fileName + ".backup"
            });
            }
#else
            throw new NotSupportedException();
#endif
        }

        /**
         * Restores in from an incremental backup
         */
        private void restoreBackupIncremental()
        {
#if !SILVERLIGHT

            try
            {
                if (fa.IsStreamElement(fileName + ".backup"))
                {
                    RAShadowFile.restoreFile(fileName + ".backup",
                                             fileName + ".data");
                    deleteBackup();
                }
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, fileName + ".backup");
            }
#else
            throw new NotSupportedException();
#endif
        }

        // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP) - text tables
        private HashMap textCacheList = new HashMap();

        public DataFileCache openTextCache(Table table, String source,
                                    bool readOnlyData, bool reversed)
        {
#if !SILVERLIGHT

            closeTextCache(table);

            if (database.getType() != DatabaseURL.S_RES
                    && !properties.isPropertyTrue(
                        EffiProzDatabaseProperties.textdb_allow_full_path))
            {
                if (source.IndexOf("..") != -1)
                {
                    throw (Error.error(ErrorCode.ACCESS_IS_DENIED, source));
                }

                String path = new FileInfo(
                        database.getPath()
                        + ".properties").Directory.FullName;

                if (path != null)
                {
                    source = path + Path.DirectorySeparatorChar + source;
                }
            }

            TextCache c = new TextCache(table, source);

            c.open(readOnlyData || filesReadOnly);
            textCacheList.put(table.getName(), c);

            return c;
#else
            throw new NotSupportedException();
#endif
        }

        public void closeTextCache(Table table)
        {

            TextCache c = (TextCache)textCacheList.remove(table.getName());

            if (c != null)
            {
                try
                {
                    c.close(true);
                }
                catch (CoreException) { }
            }
        }

        private void closeAllTextCaches(bool script)
        {

            Iterator it = textCacheList.values().iterator();

            while (it.hasNext())
            {
                TextCache textCache = ((TextCache)it.next());

                // use textCache.table to cover both cach and table readonly
                if (script && !textCache.table.isDataReadOnly())
                {
                    textCache.purge();
                }
                else
                {
                    textCache.close(true);
                }
            }
        }

        private void reopenAllTextCaches()
        {

            Iterator it = textCacheList.values().iterator();

            while (it.hasNext())
            {
                ((TextCache)it.next()).reopen();
            }
        }

        private bool isAnyTextCacheModified()
        {

            Iterator it = textCacheList.values().iterator();

            while (it.hasNext())
            {
                if (((TextCache)it.next()).isFileModified())
                {
                    return true;
                }
            }

            return false;
        }
    }
}